package lejos.pc.bconsole;

import java.awt.Component;
import java.awt.Font;
import java.awt.Color;
import java.awt.Insets;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.util.Vector;
import java.awt.Cursor;
import javax.swing.text.*;
import javax.swing.*;


// Things that are not in the core packages

/**
 * A JFC/Swing based console for the BeanShell desktop. This is a descendant of
 * the old AWTConsole.
 * 
 * Improvements by: Mark Donszelmann <Mark.Donszelmann@cern.ch> including Cut &
 * Paste
 * 
 * Improvements by: Daniel Leuck including Color and Image support, key press
 * bug workaround
 */
public class JConsole extends JScrollPane implements ConsoleInterface, Runnable, KeyListener, MouseListener, ActionListener,
		PropertyChangeListener {

	private static final long serialVersionUID = 1912627000240232170L;

	private final static String CUT = "Cut";
	private final static String COPY = "Copy";
	private final static String PASTE = "Paste";

	private DataOutputStream outData;
	private DataInputStream inData;
	private InputStream inStream;
	private PrintStream out;

	boolean debug = false;

	public InputStream getInputStream() {
		return inStream;
	}

	public Reader getIn() {
		return new InputStreamReader(inStream);
	}

	public PrintStream getOut() {
		return out;
	}

	public PrintStream getErr() {
		return out;
	}

	private int cmdStart = 0;
	private Vector<String> history = new Vector<String>();
	private String startedLine;
	private int histLine = 0;

	private JPopupMenu menu;
	private JTextPane text;
	private DefaultStyledDocument doc;

	final int SHOW_AMBIG_MAX = 10;

	// hack to prevent key repeat for some reason?
	private boolean gotUp = true;

	public JConsole() {
		this(null, null);
	}

	public JConsole(DataInputStream cin, DataOutputStream cout) {
		super();

		// Special TextPane which catches for cut and paste, both L&F keys and
		// programmatic behaviour
		doc = new DefaultStyledDocument();
		text = new JTextPane(doc) {
			private static final long serialVersionUID = 5982746406501505105L;

			@Override
			public void cut() {
				if (text.getCaretPosition() < cmdStart) {
					super.copy();
				} else {
					super.cut();
				}
			}

			@Override
			public void paste() {
				forceCaretMoveToEnd();
				super.paste();
			}
		};

		Font font = new Font("Monospaced", Font.PLAIN, 14);
		text.setText("");
		text.setFont(font);
		text.setMargin(new Insets(7, 5, 7, 5));
		text.addKeyListener(this);
		setViewportView(text);

		// create popup menu
		menu = new JPopupMenu("JConsole	Menu");
		menu.add(new JMenuItem(CUT)).addActionListener(this);
		menu.add(new JMenuItem(COPY)).addActionListener(this);
		menu.add(new JMenuItem(PASTE)).addActionListener(this);

		text.addMouseListener(this);

		// make sure popup menu follows Look & Feel
		UIManager.addPropertyChangeListener(this);

		// outPipe = cout;
		// inPipe = cin;
		outData = cout;
		inData = cin;

		// if (outData == null) {
		// outData = new PipedOutputStream();
		// try {
		// inStream = new PipedInputStream((PipedOutputStream) outData);
		// } catch (IOException e) {
		// print("Console internal	error (1)...", Color.red);
		// }
		// }
		// if (inData == null) {
		// PipedOutputStream pout = new PipedOutputStream();
		// out = new PrintStream(pout);
		// try {
		// inData = new BlockingPipedInputStream(pout);
		// } catch (IOException e) {
		// print("Console internal error: " + e);
		// }
		// }
		// Start the inpipe watcher
		new Thread(this).start();

		requestFocus();
	}

	@Override
	public void requestFocus() {
		super.requestFocus();
		text.requestFocus();
	}

	public void keyPressed(KeyEvent e) {
		type(e);
		gotUp = false;
	}

	public void keyTyped(KeyEvent e) {
		type(e);
	}

	public void keyReleased(KeyEvent e) {
		gotUp = true;
		type(e);
	}

	private synchronized void type(KeyEvent e) {
		switch (e.getKeyCode()) {
		case (KeyEvent.VK_ENTER):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				if (gotUp) {
					enter();
					resetCommandStart();
					text.setCaretPosition(cmdStart);
				}
			}
			e.consume();
			text.repaint();
			break;

		case (KeyEvent.VK_UP):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				historyUp();
			}
			e.consume();
			break;

		case (KeyEvent.VK_DOWN):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				historyDown();
			}
			e.consume();
			break;

		case (KeyEvent.VK_LEFT):
		case (KeyEvent.VK_BACK_SPACE):
		case (KeyEvent.VK_DELETE):
			if (text.getCaretPosition() <= cmdStart) {
				// This doesn't work for backspace.
				// See default case for workaround
				e.consume();
			}
			break;

		case (KeyEvent.VK_RIGHT):
			forceCaretMoveToStart();
			break;

		case (KeyEvent.VK_HOME):
			text.setCaretPosition(cmdStart);
			e.consume();
			break;

		case (KeyEvent.VK_U): // clear line
			if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
				replaceRange("", cmdStart, textLength());
				histLine = 0;
				e.consume();
			}
			break;

		case (KeyEvent.VK_ALT):
		case (KeyEvent.VK_CAPS_LOCK):
		case (KeyEvent.VK_CONTROL):
		case (KeyEvent.VK_META):
		case (KeyEvent.VK_SHIFT):
		case (KeyEvent.VK_PRINTSCREEN):
		case (KeyEvent.VK_SCROLL_LOCK):
		case (KeyEvent.VK_PAUSE):
		case (KeyEvent.VK_INSERT):
		case (KeyEvent.VK_F1):
		case (KeyEvent.VK_F2):
		case (KeyEvent.VK_F3):
		case (KeyEvent.VK_F4):
		case (KeyEvent.VK_F5):
		case (KeyEvent.VK_F6):
		case (KeyEvent.VK_F7):
		case (KeyEvent.VK_F8):
		case (KeyEvent.VK_F9):
		case (KeyEvent.VK_F10):
		case (KeyEvent.VK_F11):
		case (KeyEvent.VK_F12):
		case (KeyEvent.VK_ESCAPE):
			// only modifier pressed
			break;

		// Control-C
		case (KeyEvent.VK_C):
			if (text.getSelectedText() == null) {
				if (((e.getModifiers() & InputEvent.CTRL_MASK) > 0) && (e.getID() == KeyEvent.KEY_PRESSED)) {
					append("^C");
				}
				e.consume();
			}
			break;

		// case ( KeyEvent.VK_TAB ):
		// if (e.getID() == KeyEvent.KEY_RELEASED) {
		// String part = text.getText().substring( cmdStart );
		// //doCommandCompletion( part );
		// }
		// e.consume();
		// break;

		default:
			if ((e.getModifiers() & (InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0) {
				// plain character
				forceCaretMoveToEnd();
			}

			/*
			 * The getKeyCode function always returns VK_UNDEFINED for keyTyped
			 * events, so backspace is not fully consumed.
			 */
			if (e.paramString().indexOf("Backspace") != -1) {
				if (text.getCaretPosition() <= cmdStart) {
					e.consume();
					break;
				}
			}
			break;
		}
	}

	private void resetCommandStart() {
		cmdStart = textLength();
	}

	private void append(String string) {
		int slen = textLength();
		text.select(slen, slen);
		text.replaceSelection(string);
	}

	private String replaceRange(Object s, int start, int end) {
		String st = s.toString();
		text.select(start, end);
		text.replaceSelection(st);
		// text.repaint();
		return st;
	}

	private void forceCaretMoveToEnd() {
		if (text.getCaretPosition() < cmdStart) {
			// move caret first!
			text.setCaretPosition(textLength());
		}
		text.repaint();
	}

	private void forceCaretMoveToStart() {
		if (text.getCaretPosition() < cmdStart) {
			// move caret first!
		}
		text.repaint();
	}

	private void enter() {
		String s = getCmd();

		if (s.length() == 0) // special hack for empty return!
			s = "\n";
		else {
			history.addElement(s);
			s = s + "\n";
		}

		append("\n");
		histLine = 0;
		processLine(s);
		text.repaint();
	}

	private String getCmd() {
		String s = "";
		try {
			s = text.getText(cmdStart, textLength() - cmdStart);
		} catch (BadLocationException e) {
			// should not happen
			System.out.println("Internal JConsole Error: " + e);
		}
		return s;
	}

	private void historyUp() {
		if (history.size() == 0)
			return;
		if (histLine == 0) // save current line
			startedLine = getCmd();
		if (histLine < history.size()) {
			histLine++;
			showHistoryLine();
		}
	}

	private void historyDown() {
		if (histLine == 0)
			return;

		histLine--;
		showHistoryLine();
	}

	private void showHistoryLine() {
		String showline;
		if (histLine == 0)
			showline = startedLine;
		else
			showline = history.elementAt(history.size() - histLine);

		replaceRange(showline, cmdStart, textLength());
		text.setCaretPosition(textLength());
		text.repaint();
	}

	String ZEROS = "000";

	private void processLine(String line) {
		// Patch to handle Unicode characters
		// Submitted by Daniel Leuck
		StringBuffer buf = new StringBuffer();
		int lineLength = line.length();
		for (int i = 0; i < lineLength; i++) {
			char c = line.charAt(i);
			if (c > 127) {
				String val = Integer.toString(c, 16);
				val = ZEROS.substring(0, 4 - val.length()) + val;
				buf.append("\\u" + val);
			} else {
				buf.append(c);
			}
		}
		line = buf.toString();
		// End unicode patch

		if (outData == null)
			print("Console internal	error: cannot output ...", Color.red);
		else {
			try {
				if (debug) {
					System.err.println("Sending: " + line + "(" + line.getBytes() + ")");
				}
				outData.writeUTF(line);
				outData.flush();
			} catch (IOException e) {
				outData = null;
				throw new RuntimeException("Console pipe broken...");
			}
		}
		// text.repaint();
	}

	public void println(Object o) {
		print(String.valueOf(o) + "\n");
		text.repaint();
	}

	public void print(final Object o) {
		invokeAndWait(new Runnable() {
			public void run() {
				append(String.valueOf(o));
				resetCommandStart();
				text.setCaretPosition(cmdStart);
			}
		});
	}

	/**
	 * Prints "\\n" (i.e. newline)
	 */
	public void println() {
		print("\n");
		text.repaint();
	}

	public void error(Object o) {
		print(o, Color.red);
	}

	public void println(Icon icon) {
		print(icon);
		println();
		text.repaint();
	}

	public void print(final Icon icon) {
		if (icon == null)
			return;

		invokeAndWait(new Runnable() {
			public void run() {
				text.insertIcon(icon);
				resetCommandStart();
				text.setCaretPosition(cmdStart);
			}
		});
	}

	public void print(Object s, Font font) {
		print(s, font, null);
	}

	public void print(Object s, Color color) {
		print(s, null, color);
	}

	public void print(final Object o, final Font font, final Color color) {
		invokeAndWait(new Runnable() {
			public void run() {
				AttributeSet old = getStyle();
				setStyle(font, color);
				append(String.valueOf(o));
				resetCommandStart();
				text.setCaretPosition(cmdStart);
				setStyle(old, true);
			}
		});
	}

	public void print(Object s, String fontFamilyName, int size, Color color) {

		print(s, fontFamilyName, size, color, false, false, false);
	}

	public void print(final Object o, final String fontFamilyName, final int size, final Color color, final boolean bold,
			final boolean italic, final boolean underline) {
		invokeAndWait(new Runnable() {
			public void run() {
				AttributeSet old = getStyle();
				setStyle(fontFamilyName, size, color, bold, italic, underline);
				append(String.valueOf(o));
				resetCommandStart();
				text.setCaretPosition(cmdStart);
				setStyle(old, true);
			}
		});
	}

	// private AttributeSet setStyle(Font font) {
	// return setStyle(font, null);
	// }
	//
	// private AttributeSet setStyle(Color color) {
	// return setStyle(null, color);
	// }

	private AttributeSet setStyle(Font font, Color color) {
		if (font != null) {
			return setStyle(font.getFamily(), font.getSize(), color, font.isBold(), font.isItalic(),
					StyleConstants.isUnderline(getStyle()));
		}
		return setStyle(null, -1, color);
	}

	private AttributeSet setStyle(String fontFamilyName, int size, Color color) {
		MutableAttributeSet attr = new SimpleAttributeSet();
		if (color != null)
			StyleConstants.setForeground(attr, color);
		if (fontFamilyName != null)
			StyleConstants.setFontFamily(attr, fontFamilyName);
		if (size != -1)
			StyleConstants.setFontSize(attr, size);

		setStyle(attr);

		return getStyle();
	}

	private AttributeSet setStyle(String fontFamilyName, int size, Color color, boolean bold, boolean italic, boolean underline) {
		MutableAttributeSet attr = new SimpleAttributeSet();
		if (color != null)
			StyleConstants.setForeground(attr, color);
		if (fontFamilyName != null)
			StyleConstants.setFontFamily(attr, fontFamilyName);
		if (size != -1)
			StyleConstants.setFontSize(attr, size);
		StyleConstants.setBold(attr, bold);
		StyleConstants.setItalic(attr, italic);
		StyleConstants.setUnderline(attr, underline);

		setStyle(attr);

		return getStyle();
	}

	private void setStyle(AttributeSet attributes) {
		setStyle(attributes, false);
	}

	private void setStyle(AttributeSet attributes, boolean overWrite) {
		text.setCharacterAttributes(attributes, overWrite);
	}

	private AttributeSet getStyle() {
		return text.getCharacterAttributes();
	}

	@Override
	public void setFont(Font font) {
		super.setFont(font);

		if (text != null)
			text.setFont(font);
	}

	private void inPipeWatcher() {
		// byte[] ba = new byte[256]; // arbitrary blocking factor
		// int read;
		// while ((read = inData.read(ba)) != -1)
		// print(new String(ba, 0, read));

		String msg;

		while (true) {
			try {
				msg = inData.readUTF();
			} catch (IOException e) {
				// expect this to happen
				// e.printStackTrace();
				msg = e.getMessage();
				break;
			}
			if (msg.equals(">>>")) {
				print(msg, Color.blue);
			} else {
				print(msg);
			}
			// text.repaint();
		}
		print("Console: Input closed\n", Color.red);
	}

	public void run() {
		// System.out.println("inPipeWatcher run()");
		inPipeWatcher();
	}

	@Override
	public String toString() {
		return "Jython console";
	}

	// MouseListener Interface
	public void mouseClicked(MouseEvent event) {
		// Nothing to do
	}

	public void mousePressed(MouseEvent event) {
		if (event.isPopupTrigger()) {
			menu.show((Component) event.getSource(), event.getX(), event.getY());
		}
	}

	public void mouseReleased(MouseEvent event) {
		if (event.isPopupTrigger()) {
			menu.show((Component) event.getSource(), event.getX(), event.getY());
		}
		text.repaint();
	}

	public void mouseEntered(MouseEvent event) {
		// Nothing to do
	}

	public void mouseExited(MouseEvent event) {
		// Nothing to do
	}

	// property change
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getPropertyName().equals("lookAndFeel")) {
			SwingUtilities.updateComponentTreeUI(menu);
		}
	}

	// handle cut, copy and paste
	public void actionPerformed(ActionEvent event) {
		String cmd = event.getActionCommand();
		if (cmd.equals(CUT)) {
			text.cut();
		} else if (cmd.equals(COPY)) {
			text.copy();
		} else if (cmd.equals(PASTE)) {
			text.paste();
		}
	}

	/**
	 * If not in the event thread run via SwingUtilities.invokeAndWait()
	 */
	private void invokeAndWait(Runnable run) {
		if (!SwingUtilities.isEventDispatchThread()) {
			try {
				SwingUtilities.invokeAndWait(run);
			} catch (Exception e) {
				// shouldn't happen
				e.printStackTrace();
			}
		} else {
			run.run();
		}
	}

	/**
	 * The overridden read method in this class will not throw "Broken pipe"
	 * IOExceptions; It will simply wait for new writers and data. This is used
	 * by the JConsole internal read thread to allow writers in different (and
	 * in particular ephemeral) threads to write to the pipe.
	 * 
	 * It also checks a little more frequently than the original read().
	 * 
	 * Warning: read() will not even error on a read to an explicitly closed
	 * pipe (override closed to for that).
	 */
	public static class BlockingPipedInputStream extends PipedInputStream {
		boolean closed;

		public BlockingPipedInputStream(PipedOutputStream pout) throws IOException {
			super(pout);
		}

		@Override
		public synchronized int read() throws IOException {
			if (closed)
				throw new IOException("stream closed");

			while (super.in < 0) { // While no data */
				notifyAll(); // Notify any writers to wake up
				try {
					wait(750);
				} catch (InterruptedException e) {
					throw new InterruptedIOException();
				}
			}
			// This is what the superclass does.
			int ret = buffer[super.out++] & 0xFF;
			if (super.out >= buffer.length)
				super.out = 0;
			if (super.in == super.out)
				super.in = -1; /* now empty */
			return ret;
		}

		@Override
		public void close() throws IOException {
			closed = true;
			super.close();
		}
	}

	public void setWaitFeedback(boolean on) {
		if (on)
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
		else
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
	}

	private int textLength() {
		return text.getDocument().getLength();
	}

}
